﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using DeepEarth.Map.Core;
using DeepEarth.Map.Core.Utilities;
using DeepEarth.Map.Core.Extension;
using DeepEarth.Map.Core.Tiling;
using DeepEarth.Core;
using DeepEarth.Core.Controls;
using DeepEarth.Core.Persistence;
using DeepEarth.Core.Utilities;

#if !WINDOWS_PHONE
using System.Reactive.Linq;
using System.Windows.Markup;
#else
using Microsoft.Phone.Reactive;
#endif

namespace DeepEarth.Map
{
    #if !WINDOWS_PHONE
    [ContentProperty("Children")]
    #endif
    public partial class Map : Grid, IMap
    {
        const int DEFAULT_PRECISION = 10;
        
        /// <summary>
        /// The blank master MSI used for tracking input to the map.
        /// </summary>
        readonly MultiScaleImage masterMsi = new MultiScaleImage { CacheMode = new BitmapCache(), Source = new MasterMsiTileSource() };

        /// <summary>
        /// The list of non-master MSIs. These correspond to the Mode and SecondaryModes of the current view.
        /// </summary>
        readonly List<MultiScaleImage> slaveMsis = new List<MultiScaleImage>();
        readonly Image modeChangeImage;

        #region Properties


        public MapView View
        {
            get { return (MapView)GetValue(ViewProperty); }
            set { SetValue(ViewProperty, value); }
        }

        public static readonly DependencyProperty UseSpringsProperty = DependencyProperty.Register("UseSprings", typeof(bool), typeof(Map), new PropertyMetadata(true, UseSpringsChanged));

        public bool UseSprings
        {
            get { return (bool)GetValue(UseSpringsProperty); }
            set { SetValue(UseSpringsProperty, value); }
        }

        public static readonly DependencyProperty IsInteractiveProperty = DependencyProperty.Register("IsInteractive", typeof(bool), typeof(Map), new PropertyMetadata(true));
        public bool IsInteractive
        {
            get { return (bool)GetValue(IsInteractiveProperty); }
            set { SetValue(IsInteractiveProperty, value); }
        }

        Point targetViewportOrigin;
        double _targetViewportWidth = 1;

        double TargetViewportWidth
        {
            get
            {
                return _targetViewportWidth;
            }
            set
            {
                if (value != 0)
                {
                    if (this.masterMsi.ActualWidth > 0)
                    {
                        _targetViewportWidth = Math.Min(MaximumViewportWidth(this.masterMsi.ActualWidth), value);
                    }
                    else
                    {
                        _targetViewportWidth = value;
                    }
                }
            }
        }

        public double TargetZoomLevel
        {
            get { var zoom = ViewportWidthToZoom(TargetViewportWidth); return 1 + Math.Log(zoom, 2); }
        }

        Location TargetCenter
        {
            get
            {
                var targetLogicalCentre = new Point(targetViewportOrigin.X + (TargetViewportWidth / 2), targetViewportOrigin.Y + ((TargetViewportWidth / 2) * (ActualHeight / ActualWidth)));
                return CoordinateTransformation.LogicalToGeographic(targetLogicalCentre);
            }
        }

        Point TargetViewportCenter
        {
            set
            {
                targetViewportOrigin = new Point(value.X - TargetViewportWidth / 2, value.Y - ((TargetViewportWidth / 2) * (ActualHeight/ActualWidth)));
                SyncMapToTarget();
            }
        }

        public bool IsIdle
        {
            get
            {
                //Are all MSI's idle?
                return masterMsi.IsIdle && slaveMsis.All(msi => msi.IsIdle);
            }
        }

        public double MasterMsiViewportWidth
        {
            get { return masterMsi.ViewportWidth; }
        }

        public new Layer Children
        {
            get
            {
                return baseLayer;
            }
        }
       
        #endregion

        public LocationRectangle BoundingRectangle
        {
            get
            {
                var geoOrigin = PixelToGeographic(masterMsi.ViewportOrigin);

                var pixelExtent = new Point(masterMsi.ViewportOrigin.X + masterMsi.ActualWidth,
                                            masterMsi.ViewportOrigin.Y + masterMsi.ActualHeight);
                var geoExtent = PixelToGeographic(pixelExtent);
                return new LocationRectangle(geoOrigin, geoExtent);
            }
        }

        LayerGroup baseLayerGroup;
        Layer baseLayer;

        public Map()
        {
            base.Children.Add(masterMsi);
            modeChangeImage = new Image { IsHitTestVisible = false };
            base.Children.Add(modeChangeImage);

            Panel overlayPanel;
            Panel interactionSurface;
            var baselayers = MapHelper.InitializeSurfaces(this, false, base.Children, out baseLayerGroup, out baseLayer, out overlayPanel, out interactionSurface);

            this.OverlayPanel = overlayPanel;
            this.InteractionSurface = interactionSurface;

            BaseRasterOverlayCollection = baselayers.Item1;
            BaseDynamicLayer = baselayers.Item2;
            BaseVectorLayerCollection = baselayers.Item3;
            BaseLabelLayer = baselayers.Item4;

            SizeChanged += (o, e) =>
            {
                //resync zoom
                var zoom = (masterMsi.ActualWidth / 512) * (1 / TargetViewportWidth);
                //verify bounds
                if (zoom < 1)
                {
                    TargetViewportWidth = ZoomToViewportWidth(zoom);
                    View.ViewportWidth = TargetViewportWidth;
                    targetViewportOrigin = originFromGeographicCentre(View.Center, masterMsi, TargetViewportWidth);

                    SyncMapToTarget();
                }
                else
                {
                    AnimateBlur();
                }

                double width = masterMsi.ActualWidth;
                double height = masterMsi.ActualHeight;

                RectangleGeometry clippingRectangle = new RectangleGeometry();
                clippingRectangle.Rect = new Rect(0, 0, width, height);
                this.Clip = clippingRectangle;
            };

            masterMsi.ViewportChanged += (o, e) => ViewChanged();

            masterMsi.MotionFinished += (o, e) =>
                {
                    if (ViewChangeEnd != null)
                    {
                        ViewChangeEnd(this, e);
                    }
                };
           

            masterMsi.ImageOpenSucceeded += masterMsi_ImageOpenSucceeded;

            InitializeGestureHandlers();

            CreateCommands();

            View = new MapView();

            this.Loaded += Map_Loaded;

            this.NavigateCommand = new LambdaCommand<object>(o =>
            {
                if (o is ILocatable)
                {
                    ILocatable locatable = o as ILocatable;
                    if (locatable.Position != null)
                    {
                        this.View.SetLocation(locatable.Position, this.View.ViewportWidth);
                    }
                }
            });
        }

        void Map_Loaded(object sender, RoutedEventArgs e)
        {
            this.Loaded -= Map_Loaded;

#if !WINDOWS_PHONE
            if (HasPersistenceName)
            {
                Restore();

                Application.Current.Exit += (o, evt) =>
                {
                    SaveState();
                };

            }
#endif
        }


        bool mouseLeftDown = false;
        Point previousPoint;

        private void InitializeGestureHandlers()
        {
            #region Phone
#if WINDOWS_PHONE
            this.ManipulationStarted += new EventHandler<ManipulationStartedEventArgs>(Map_ManipulationStarted);
            this.ManipulationDelta += new EventHandler<ManipulationDeltaEventArgs>(Map_ManipulationDelta);
            this.ManipulationCompleted += new EventHandler<ManipulationCompletedEventArgs>(Map_ManipulationCompleted);

            var doubleclickEvent = this.GetDoubleClick();

            doubleclickEvent.Subscribe(p =>
            {
                if (IsInteractive && maybeClick)
                {
                    isDoubleClick = true;
                    var isShiftDown = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
                    var adjustment = isShiftDown ? 0.7 : 1.3;
                    var logicalPoint = masterMsi.ElementToLogicalPoint(p.Value.EventArgs.GetPosition(this));
                    Zoom(adjustment, logicalPoint);
                }
            });
#endif
            #endregion

            #region Not Phone
#if !WINDOWS_PHONE

            // don't want to capture mouse too early. Wait for first mousemove after a mouse left down to start capture
            var dragEvent = this.GetMouseDragWithDelayedCapture();

            dragEvent.Subscribe(p =>
            {
                if (IsInteractive)
                {
                    targetViewportOrigin = new Point(targetViewportOrigin.X - p.PixelDelta.X / masterMsi.ActualWidth * masterMsi.ViewportWidth, targetViewportOrigin.Y - p.PixelDelta.Y / masterMsi.ActualWidth * masterMsi.ViewportWidth);
                    SyncMapToTarget();
                }
            });


            var doubleclickEvent = this.GetDoubleClick();

            doubleclickEvent.Subscribe(p =>
            {
                if (IsInteractive)
                {
                    var isShiftDown = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
                    var adjustment = isShiftDown ? 0.7 : 1.3;
                    var logicalPoint = masterMsi.ElementToLogicalPoint(p.Value.EventArgs.GetPosition(this));
                    Zoom(adjustment, logicalPoint);
                }
            });

            var mousewheelEvent = this.GetMouseWheel();
            mousewheelEvent.Subscribe(p =>
            {
                if (IsInteractive)
                {
                    var delta = p.EventArgs.Delta > 0 ? 1.3 : 0.7;
                    var logicalPoint = masterMsi.ElementToLogicalPoint(p.EventArgs.GetPosition(this));
                    Zoom(delta, logicalPoint);
                }
            });
#endif

            #endregion
        }

        #region Phone Manipulation Handlers
#if WINDOWS_PHONE
        
        bool isDoubleClick = false;
        bool maybeClick = false;        

        void Map_ManipulationStarted(object sender, ManipulationStartedEventArgs e)
        {
            isDoubleClick = false;
            maybeClick = true;
        }
         
        void Map_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
        {
            maybeClick = false;
            isDoubleClick = false;

            targetViewportOrigin = new Point(targetViewportOrigin.X - e.DeltaManipulation.Translation.X / masterMsi.ActualWidth * masterMsi.ViewportWidth, targetViewportOrigin.Y - e.DeltaManipulation.Translation.Y / masterMsi.ActualWidth * masterMsi.ViewportWidth);
           
            double scaleFactor = Math.Sqrt(Math.Pow(e.DeltaManipulation.Scale.X, 2) + Math.Pow(e.DeltaManipulation.Scale.Y, 2));
            if (scaleFactor != 0)
            {
                TargetViewportWidth /= scaleFactor;
            }
            SyncMapToTarget();

            
        }

        void Map_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
        {           
            if (maybeClick)
            {
                System.Threading.ThreadPool.QueueUserWorkItem((state) =>
                    {
                        System.Threading.Thread.Sleep(300);
                        Dispatcher.BeginInvoke(() =>
                        {
                            if (!isDoubleClick )
                            {
                                TargetViewportCenter = masterMsi.ElementToLogicalPoint(e.ManipulationOrigin);
                            }
                        }
                        );
                    }
                );
            }
        }

#endif
        #endregion

        #region Property Changed Handlers

        static void ViewPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var self = (Map)sender;
            if (self.viewChangeSubscription != null)
            {
                self.viewChangeSubscription.Dispose();
            }
            if (args.OldValue != null)
            {
                ((MapView)args.OldValue).SecondaryTileSourceProviders.CollectionChanged -= self.CurrentView_SecondaryModesChanged;
            }

            if (args.NewValue != null)
            {
                MapView newView = args.NewValue as MapView;

                self.TargetViewportWidth = newView.ViewportWidth;
                Point intermed = originFromGeographicCentre(self.View.Center, self.masterMsi, self.TargetViewportWidth);

                self.targetViewportOrigin = intermed;
                self.SyncMapToTarget();
                self.SynchroniseModes();

#if !WINDOWS_PHONE
                var observableProperties = Observable.FromEventPattern<PropertyChangedEventArgs>(newView, "PropertyChanged").Where((i) => self.listenToViewChanges);
#else
                var observableProperties = Observable.FromEvent<PropertyChangedEventArgs>(newView, "PropertyChanged").Where((i) => self.listenToViewChanges);
#endif
                self.viewChangeSubscription = observableProperties.Subscribe(e => { self.CurrentView_PropertyChanged(self, e.EventArgs); });
                self.View.SecondaryTileSourceProviders.CollectionChanged += self.CurrentView_SecondaryModesChanged;
            }
        }

        IDisposable viewChangeSubscription = null;
        bool listenToViewChanges = true;

        /// <summary>
        /// Called when one of the properties on the current view changes
        /// </summary>
        /// 

        void CurrentView_PropertyChanged(object sender, PropertyChangedEventArgs args)
        {
            string changedProperty = args.PropertyName;

            switch (changedProperty)
            {
                case MapView.CENTER:
                    if (ViewDiffersFromTarget(DEFAULT_PRECISION))
                    {
                        this.targetViewportOrigin = originFromGeographicCentre(View.Center, this.masterMsi, this.TargetViewportWidth);
                        SyncMapToTarget();
                    }
                    break;

                case MapView.ZOOM_LEVEL:
                case MapView.VIEWPORT_WIDTH:
                    if (ViewDiffersFromTarget(DEFAULT_PRECISION))
                    {
                        this.TargetViewportWidth = View.ViewportWidth;
                        this.targetViewportOrigin = originFromGeographicCentre(View.Center, this.masterMsi, this.TargetViewportWidth);
                        SyncMapToTarget();
                    }
                    break;
                case MapView.TILE_SOURCE_PROVIDER :
                case MapView.PROVIDER_TILESOURCES :
                    SynchroniseModes();
                    break;
            }           
        }

        private static void UseSpringsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var map = (Map)sender;
            map.masterMsi.UseSprings = (bool)args.NewValue;
        }

        /// <summary>
        /// Called when there's a change in the set of secondary modes on the current view.
        /// </summary>
        void CurrentView_SecondaryModesChanged(object sender, NotifyCollectionChangedEventArgs args)
        {
            SynchroniseModes();
        }
        #endregion

        /// <summary>
        /// Updates all of the slave MSIs according to the supplied set of secondary modes
        /// </summary>
        void SynchroniseModes()
        {
            var modes = View.TileSourceProvider.Concat(View.SecondaryTileSourceProviders).ToArray();

            // Go through each one, fix it to match, or delete it, or add it
            int msiIndex = 0;
            foreach (var modebase in modes)
            {
                var mode = modebase as BaseTileSourceProvider;

                // Initially the mode may not be set
                if (mode == null)
                {
                    break;
                }

                // Grab all MSIs attached to this mode
                var msis = slaveMsis.Skip(msiIndex).TakeWhile(m => ((BaseTileSourceProvider)m.Tag).Mode == mode.Mode && (!mode.Mode.HasDates || ((BaseTileSourceProvider)m.Tag).Date == mode.Date)).ToArray();

                // Add, remove or update all the MSIs for this mode
                for (int i = 0; i < Math.Max(msis.Length, mode.TileSources.Count()); i++)
                {
                    var msi = msis.Length > i ? msis[i] : null;
                    var source = mode.TileSources.Count() > i ? mode.TileSources.ElementAt(i) : null;
                    if (msi == null)
                        AddTileLayer(mode, source, msiIndex + i);
                    else if (source == null)
                    {
                        base.Children.Remove(msi);
                        slaveMsis.Remove(msi);
                    }
                    else if (msi.Source != source)
                        msi.Source = source;
                    else
                        msi.Opacity = mode.Opacity;
                }

                msiIndex += mode.TileSources.Count();
            }

            // Remove any leftover MSIs
            for (int i = slaveMsis.Count - 1; i >= msiIndex; i--)
            {
                base.Children.Remove(slaveMsis[i]);
                slaveMsis.RemoveAt(i);
            }
        }

        #region Commands
        /// <summary>
        /// Initialises the command properties
        /// </summary>
        void CreateCommands()
        {
            SetModeCommand = new LambdaCommand<object>(SetModeExecuted);
        }

        /// <summary>
        /// A command for setting the map mode. The CommandParameter is used to indicate which mode to change to.
        /// Parameters must be either MapModes, NearMapMode values or a string version of the NearMapMode value.
        /// </summary>
        public ICommand SetModeCommand { get; private set; }

        /// <summary>
        /// Implementation of the SetModeCommand
        /// </summary>
        void SetModeExecuted(object parameter)
        {
            if (parameter is BaseTileSourceProvider)
                View.TileSourceProvider = (BaseTileSourceProvider)parameter;
        }
        #endregion

        #region Events
        public event EventHandler ViewChangeOnFrame;
        public event EventHandler ViewChangeEnd;
        #endregion

        void AddTileLayer(BaseTileSourceProvider provider, MultiScaleTileSource source, int index)
        {
            var msi = new MultiScaleImage
            {
                CacheMode = new BitmapCache(),
                IsHitTestVisible = false,
                Source = source,
                UseSprings = false,
                Tag = provider
            };
            if (provider.Mode.HasLabels || provider.ShowStreets)
            {
                msi.BlurFactor = calculateTargetBlur(TargetZoomLevel);
            }
            msi.SetBinding(OpacityProperty, new Binding("Opacity") { Source = provider });
            slaveMsis.Insert(index, msi);

            //add nice transistion effect
            //mode.Opacity = 0;
            //FadeInMode(mode);

            //TODO: implement a proper zindex

            base.Children.Insert(2 + index, msi);

            ViewChanged();
            //kick off blur sync
            //AnimateBlur();
        }

        void masterMsi_ImageOpenSucceeded(object sender, RoutedEventArgs e)
        {
            this.targetViewportOrigin = originFromGeographicCentre(View.Center, this.masterMsi, this.TargetViewportWidth);
            SyncMapToTarget();
            masterMsi.ImageOpenSucceeded -= masterMsi_ImageOpenSucceeded;

            ViewChanged();
        }


        // MasterMSI view changed handler
        void ViewChanged()
        {
            if (ActualWidth == 0 || ActualHeight == 0)
            {
                // Still in layout do nothing.
                return;
            }

            if (ViewChangeOnFrame != null)
                ViewChangeOnFrame(this, EventArgs.Empty);

            var syncViewportWidth = masterMsi.ViewportWidth;
            var syncViewportOrigin = masterMsi.ViewportOrigin;
            if (!UseSprings)
            {
                syncViewportWidth = TargetViewportWidth;
                syncViewportOrigin = targetViewportOrigin;
            }
            //Raster layers
            foreach (var msi in slaveMsis)
            {
                msi.ViewportWidth = masterMsi.ViewportWidth;// syncViewportWidth;
                msi.ViewportOrigin = masterMsi.ViewportOrigin;// syncViewportOrigin;
            }

            // Update back to the CurrentView
            if (View != null)
            {
                if (ViewDiffersFromTarget(DEFAULT_PRECISION))
                {
                    listenToViewChanges = false;
                    View.SetLocation(TargetCenter, TargetViewportWidth);
                    listenToViewChanges = true;
                }
            }
        }

        #region MSI View Manipulation
        private void SyncMapToTarget()
        {
            if (masterMsi.ActualWidth > 0)
            {
                //convert Location to logic point.
                var centreLogical = CoordinateTransformation.GeographicToLogical(TargetCenter);
                var newOrigin = originFromLogicalCentreAtWidth(centreLogical, masterMsi, TargetViewportWidth);

                //calculate the ViewportOrgin from new centre logical
                targetViewportOrigin = newOrigin;

                masterMsi.ViewportOrigin = targetViewportOrigin;
                masterMsi.ViewportWidth = TargetViewportWidth;

                //kick off blur sync
                AnimateBlur();
            }
        }

        void Zoom(double delta, Point logicalPoint)
        {
            if (DisableZoom)
            {
                return;
            }


            var oldZoom = ViewportWidthToZoom(this.TargetViewportWidth);
            double newZoom = oldZoom * delta;
            if (newZoom < 1)
            {
                newZoom = 1;
                //correct delta
                delta = newZoom / oldZoom;
            }

            targetViewportOrigin.X += (((logicalPoint.X - targetViewportOrigin.X) / masterMsi.ViewportWidth) * ((1 - (1 / delta)) * masterMsi.ViewportWidth));
            targetViewportOrigin.Y += (((logicalPoint.Y - targetViewportOrigin.Y) / masterMsi.ViewportWidth) * ((1 - (1 / delta)) * masterMsi.ViewportWidth));

            TargetViewportWidth = ZoomToViewportWidth(newZoom);

            masterMsi.ZoomAboutLogicalPoint(delta, logicalPoint.X, logicalPoint.Y);


            //kick off blur sync
            AnimateBlur();
        }
        #endregion

        #region Unit Translations

        // The MultiscaleImage control will become invisible if too large a viewport width is request given the physical
        // size of the control.
        static double MaximumViewportWidth(double msiWidth)
        {
            return msiWidth / 512.0;
        }

        private double ZoomToViewportWidth(double zoom)
        {
            return 1 / (zoom / (masterMsi.ActualWidth / 512));            
        }

        private double ViewportWidthToZoom(double viewportWidth)
        {
            return (masterMsi.ActualWidth / viewportWidth) / 512;
        }

        public Point GeographicToPixel(Location location)
        {
            return CoordinateTransformation.GeographicToPixel(location, masterMsi);
        }

        public Location PixelToGeographic(Point point)
        {
            return CoordinateTransformation.PixelToGeographic(point, masterMsi);
        }

        public Point LogicalToPixel(Point point)
        {
            return CoordinateTransformation.LogicalToPixel(point, masterMsi);
        }

        #endregion

        bool ViewDiffersFromTarget(int precision)
        {
            Point currentViewOrigin = originFromGeographicCentre(View.Center, this.masterMsi, View.ViewportWidth);
            return Math.Round(currentViewOrigin.X, precision) != Math.Round(targetViewportOrigin.X, precision) || Math.Round(currentViewOrigin.Y, precision) != Math.Round(targetViewportOrigin.Y, precision) || Math.Round(View.ViewportWidth, precision) != Math.Round(TargetViewportWidth, precision);
        }

        void FadeInMode(BaseTileSourceProvider mode)
        {
            var sb = new Storyboard();
            var anim = new DoubleAnimation { Duration = new Duration(TimeSpan.FromSeconds(10)), To = 1 };
            Storyboard.SetTarget(anim, mode);
            Storyboard.SetTargetProperty(anim, new PropertyPath("Mode.Opacity"));
            sb.Children.Add(anim);
            sb.Begin();

            ////capture current screen in writeable bitmap
            //var bitmap = new WriteableBitmap(this, new TranslateTransform());
            //modeChangeImage.Source = bitmap;

            ////kick off animation
            //var sb = new Storyboard();
            //var daf1 = new DoubleAnimationUsingKeyFrames { Duration = new Duration(new TimeSpan(0, 0, 0, 1, 500)) };
            //daf1.KeyFrames.Add(new EasingDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)), Value = 1 });
            //daf1.KeyFrames.Add(new EasingDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(1.0)), Value = 1 });
            //daf1.KeyFrames.Add(new EasingDoubleKeyFrame { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(1.5)), Value = 0 });
            //Storyboard.SetTarget(daf1, modeChangeImage);
            //Storyboard.SetTargetProperty(daf1, new PropertyPath("UIElement.Opacity"));
            //sb.Children.Add(daf1);
            //sb.Begin();
        }

        void AnimateBlur()
        {
            var curLevel = TargetZoomLevel;
            if (curLevel > 0 && curLevel < int.MaxValue)
            {
                var targetBlur = calculateTargetBlur(curLevel);
                var kt = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(1.5));

                //foreach (var msi in slaveMsis.Where(m => ((BaseTileSource)m.Source).Mode.HasLabels))
                //    createBlurForMSI(targetBlur, kt, msi);
            }
        }

        void createBlurForMSI(double targetBlur, KeyTime kt, MultiScaleImage msi)
        {
            if (UseSprings)
            {
                var sb = new Storyboard();
                var daf = new DoubleAnimationUsingKeyFrames();
                var sdkf = new SplineDoubleKeyFrame { Value = targetBlur, KeyTime = kt };
                var ksf = new KeySpline { ControlPoint1 = new Point(0.15, 0.475), ControlPoint2 = new Point(0, 1) };
                sdkf.KeySpline = ksf;
                daf.KeyFrames.Add(sdkf);
                Storyboard.SetTarget(daf, msi);
                Storyboard.SetTargetProperty(daf, new PropertyPath("BlurFactor"));
                sb.Children.Add(daf);
                sb.Begin();
            }
            else
            {
                msi.BlurFactor = targetBlur;
            }

        }

        #region Utilities


        static Point originFromGeographicCentre(Location centerGeographic, MultiScaleImage multiScaleImage, double viewportWidth)
        {
            var centerLogical = CoordinateTransformation.GeographicToLogical(centerGeographic);
            return originFromLogicalCentreAtWidth(centerLogical, multiScaleImage, viewportWidth);
        }

        static Point originFromLogicalCentre(Point centreLogical, MultiScaleImage multiScaleImage)
        {
            return originFromLogicalCentreAtWidth(centreLogical, multiScaleImage, multiScaleImage.ViewportWidth);
        }

        static Point originFromLogicalCentreAtWidth(Point centreLogical, MultiScaleImage multiScaleImage, double viewportWidth)
        {
            return new Point(centreLogical.X - (viewportWidth / 2), centreLogical.Y - ((viewportWidth / (multiScaleImage.ActualWidth / multiScaleImage.ActualHeight) / 2)));
        }

        static Point logicalCentre(MultiScaleImage multiScaleImage)
        {
            return multiScaleImage.ElementToLogicalPoint(new Point(multiScaleImage.ActualWidth / 2, multiScaleImage.ActualHeight / 2));
        }

        static double calculateTargetBlur(double curLevel)
        {
            return (1 / (2 - (curLevel - (int)curLevel)) * 2);
        }
        #endregion

        class MasterMsiTileSource : MultiScaleTileSource
        {
            const int MsiSize = 2097152; // 2^21
            const int TileSize = 256;

            public MasterMsiTileSource() : base(MsiSize, MsiSize, TileSize, TileSize, 0)
            {
            }

            protected override void GetTileLayers(int tileLevel, int tilePositionX, int tilePositionY, IList<object> tileImageLayerSources)
            {
            }
        }

        #region Persistence

#if !WINDOWS_PHONE

        private const string ViewportWidthPersistencePropertyKey = "ViewportWidth";
        private const string CenterPersistencePropertyKey = "Center";
        private string persistenceName;

        private bool HasPersistenceName
        {
            get
            {
                this.persistenceName = Persist.GetName(this);
                return !string.IsNullOrEmpty(persistenceName);
            }
        }

        private void Restore()
        {
            string centerSerialized = Persist.Storage[persistenceName, CenterPersistencePropertyKey];
            string viewportWidthString = Persist.Storage[persistenceName, ViewportWidthPersistencePropertyKey];

            if (!string.IsNullOrEmpty(centerSerialized) && !string.IsNullOrEmpty(viewportWidthString))
            {
                Location center = JsonSerializer.Deserialize<Location>(centerSerialized);
                double viewportWidth = double.Parse(viewportWidthString);
                this.View.SetLocation(center, viewportWidth);
            }
        }

        private void SaveState()
        {
            if (HasPersistenceName)
            {
                string serialized = JsonSerializer.Serialize(this.View.Center);
                Persist.Storage[persistenceName, CenterPersistencePropertyKey] = serialized;
                Persist.Storage[persistenceName, ViewportWidthPersistencePropertyKey] = this.View.ViewportWidth.ToString();
            }
        }

#endif

        #endregion

        #region IGeoItemsContainer

        private TemplateSelector templateSelector;
        public TemplateSelector TemplateSelector
        {
            get
            {
                return templateSelector;
            }
            set
            {
                templateSelector = value;
                baseLayerGroup.TemplateSelector = value;
            }
        }

        private TemplateSelector labelTemplateSelector;
        public TemplateSelector LabelTemplateSelector
        {
            get
            {
                return labelTemplateSelector;
            }
            set
            {
                labelTemplateSelector = value;
                baseLayerGroup.LabelTemplateSelector = value;
            }
        }


        public GeoItems GeoItems
        {
            get { return (GeoItems)GetValue(GeoItemsProperty); }
            set { SetValue(GeoItemsProperty, value); }
        }

        // Using a DependencyProperty as the backing store for GeoItems.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty GeoItemsProperty =
            DependencyProperty.Register("GeoItems", typeof(GeoItems), typeof(Map), new PropertyMetadata(GeoItemsPropertyChanged));

        private static void GeoItemsPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            Map self = sender as Map;

            if (args.NewValue != null)
            {
                GeoItems geoItems = args.NewValue as GeoItems;

                self.baseLayerGroup.ItemsSource = geoItems.ViewModels;

                if (self.ApplyDefaultNavigationCommand)
                {
                    geoItems.NavigateCommand = MultiCommand.Combine(geoItems.NavigateCommand, self.NavigateCommand);
                }
            }


        }
        #endregion

        public bool applyDefaultNavigationCommand = true;
        public bool ApplyDefaultNavigationCommand
        {
            get { return applyDefaultNavigationCommand; }
            set { applyDefaultNavigationCommand = value; }
        }

        public ICommand NavigateCommand
        {
            get { return (ICommand)GetValue(NavigateCommandProperty); }
            set { SetValue(NavigateCommandProperty, value); }
        }

        public static readonly DependencyProperty NavigateCommandProperty =
            DependencyProperty.Register("NavigateCommand", typeof(ICommand), typeof(Map), null);

    }
}